/***********************************************************************
 * SimpleSDL For D Programming Language
 *
 * This is a very simple wrapper to DerelictSDL in D Programming 
 * Language.
 *
 * Currently it is useless at all.
 *
 * Usage:
 * -------------------------------------------------------------------
 * void main()
 * {
 *     // Create an main window which size is 1024x768.
 *     auto display = new SDLDisplay (1024,768);
 * 
 *     // Set title of main window
 *     display.setTitle ("Hello SimpleSDL");
 * 
 *     // Create an background SDL Surface from a image file.
 *     auto background = new SDLSurface ("StreetTree.jpg");
 * 
 *     // Create an foreground SDL Surface from a image file.
 *     auto foreground = new SDLSurface ("ga15.gif");
 * 
 *     // Create layer 1 by background surface
 *     auto layer1 = display.addLayer (background, "background");
 * 
 *     // Create layer 1 by foreground surface at (300, 200) of main window.
 *     auto layer2 = display.addLayer (foreground, "foreground", 300, 200);
 * 
 *     // Set foreground to half transparent
 *     layer2.alpha = 210;
 * 
 *     // Update main window.
 *     display.render ();
 * 
 *     // The following is same with SDL C Library.
 *     bool running = true;
 *     SDL_Event  event;
 *     while (running) {
 *         SDL_WaitEvent (&event);
 *         Stdout.format ("Event Type:{}", event.type).newline;
 *     }
 * }
 * -------------------------------------------------------------------
 *
 * Authors:   Brian Hsu (brianhsu.hsu [nospamat] gmail [dot] com)
 * Copyright: Brian Hsu, 2008.
 * License:   GNU GPL
 *
 *   This file is part of SimpleSDL.
 *
 *   SimpleSDL is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   SimpleSDL is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with SimpleSDL.  If not, see http://www.gnu.org/licenses/.
 */

module SimpleSDL.sdl;

private import SimpleSDL.main;

private import tango.io.Stdout;
private import tango.stdc.stringz;

private import derelict.sdl.sdl;
private import derelict.sdl.image;
private import derelict.sdl.ttf;
private import Integer = tango.text.convert.Integer;

/**************************************************************
 * Alias of char []
 *************************************************************/
private alias char [] String;

public enum Alpha
{
    TRANSPARENT = SDL_ALPHA_TRANSPARENT, 
    OPAQUE      = SDL_ALPHA_OPAQUE 
};

/**************************************************************
 * Static Module Init
 *
 * This will load SDL Image and SDL TTF Library.
 *************************************************************/
static this ()
{
    Stdout.format ("Init SDL").newline;
    DerelictSDLImage.load ();
    DerelictSDLttf.load ();
    Stdout.format ("Init SDL Done").newline;
}

/***************************************************************
 * SDL Layer Interface
 **************************************************************/
interface SDLLayer
{
    /***************************************************
     * Get Layer Top-Left X Coordinate
     *
     * Returns:
     *     Top-Left X Coordinate of this layer.
     **************************************************/
    public int x ();

    /***************************************************
     * Get Layer Top-Left Y Coordinate
     *
     * Returns:
     *     Top-Left Y Coordinate of this layer.
     **************************************************/
    public int y ();

    /***************************************************
     * Get Width of Layer
     *
     * Returns:
     *     Width of this layer.
     **************************************************/
    public uint width ();

    /***************************************************
     * Get Height of Layer
     *
     * Returns:
     *     Height of this layer.
     **************************************************/
    public uint height ();

    /***************************************************
     * Get Alpha of Layer
     *
     * Returns:
     *     Alpha value of this layer.
     **************************************************/
    public ubyte alpha ();

    /***************************************************
     * Set Alpha of Layer
     *
     * Params:
     *     aplha = Alpha value for layer, 0 is transparent
     *             and 255 is opaque.
     **************************************************/
    public void alpha (ubyte alpha);

    /***************************************************
     * Is this layer hidden?
     *
     * Returns:
     *     Return true if this layer is hidden, 
     *     otherwise return false.
     **************************************************/
    public bool hide ();

    /***************************************************
     * Hide/Show Layer.
     *
     * Params:
     *     isHide = Should this layer be hidden?
     **************************************************/
    public void hide (bool isHide);

    /***************************************************
     * Move Layer to (X,Y).
     *
     * Params:
     *     x = New Top-Left X Coordinate of this layer.
     *     y = New Top-Left Y Coordinate of this layer.
     **************************************************/
    public void move (int x, int y);

    /***************************************************
     * Move Animation (X-Axis)
     *
     * Params:
     *     offset = Offset of move in pixel
     *     step   = How many pixel a frame
     **************************************************/
    public void moveX (int offset, uint step);

    /***************************************************
     * Move Animation (Y-Axis)
     *
     * Params:
     *     offset = Offset of move in pixel
     *     step   = How many pixel a frame
     **************************************************/
    public void moveY (int offset, uint step);

    /***************************************************
     * Move Animation (Both X-Axis and Y-Axis)
     *
     * Params:
     *     offset = Offset of move in pixel
     *     step   = How many pixel a frame
     **************************************************/
    public void moveXY (int offsetX, int offsetY, uint stepX, uint stepY);

    /**************************************************
     * Fade Out
     *
     * Params:
     *     rate = The fade out rate
     *************************************************/
    public void fadeOut (float rate = 1);

    /**************************************************
     * Fade In
     *
     * Params:
     *     rate = The fade out rate
     *************************************************/
    public void fadeIn (float rate = 1);

    /***************************************************
     * Is this layer valid?
     *
     * Returns:
     *     If this layer has been removed, return true.
     *     Otherwise, return false.
     **************************************************/
    public bool valid ();

    /***************************************************
     * Remove Layer
     **************************************************/
    public void remove ();

    /**************************************************
     * Get Name of Layer
     *
     * Returns:
     *     Name of this layer.
     *************************************************/
    public String name ();

    /**************************************************
     * SDLSurface of layer
     *
     * Returns:
     *     The SDL Surface of this layer.
     *************************************************/
    public SDLSurface surface ();
}

/******************************************************
 * SDLSurface
 *
 * This class is a wrapper of SDL_Surface
 *****************************************************/
class SDLSurface
{
    private SDL_Surface * mSurfacePointer;

    /*******************************************************
     * Create SDL Surface
     *
     * Params:
     *     width  = Width of new surface.
     *     height = Height of new surface.
     *     bitsPerPixel = Bits per pixel.
     *     flags = Flags used by SDL to create surface.
     *             Default is SDL_HWSURFACE | SDL_SRCALPHA
     ******************************************************/
    public this (int width, int height, 
                 int bitsPerPixel = 32,
                 uint flags       = SDL_HWSURFACE|SDL_SRCALPHA,
                 uint redMask     = 0, uint greenMask = 0, 
                 uint blueMask    = 0, uint alphaMask = 0)
    {
        mSurfacePointer = SDL_CreateRGBSurface (flags, width, height, 
                                                bitsPerPixel, redMask, 
                                                greenMask, blueMask, 
                                                alphaMask);

        if (mSurfacePointer is null) {
            String message = fromStringz (SDL_GetError());
            throw new Exception ("Cannot create SDLSurface:" ~ message);
        }
    }

    /************************************************************
     * Create SDL Surface From Image File
     *
     * Params:
     *     image = Filename of image file.
     ***********************************************************/
    public this (String image)
    {
        mSurfacePointer = IMG_Load (toStringz(image));

        if (mSurfacePointer is null) {
            auto message = fromStringz (SDL_GetError());
            throw new Exception ("Cannot create an display:" ~ message);
        }

        // Check which type of transparent background current image is.
        bool colorkey = (mSurfacePointer.flags & SDL_SRCCOLORKEY) != 0;
        bool alpha    = (mSurfacePointer.flags & SDL_SRCALPHA) != 0;

        // If image is any kind of transparent background
        if (colorkey) {
            // Get the transparent background color mask
            uint value = mSurfacePointer.format.colorkey;

            Stdout.format ("Set Color Key...").newline;
            // Set color key of transparent background.
            SDL_SetColorKey (mSurfacePointer, SDL_SRCCOLORKEY|SDL_RLEACCEL, 
                             value);
        } else if (alpha) {
            uint value;
            bool found = findColorKey (mSurfacePointer, value);
            Stdout.format ("Set Alpha Color Key...").newline;

            if (found) {
                Stdout.format ("Set Alpha Color Key...Found").newline;

                // Set color key of transparent background.
                SDL_SetColorKey (mSurfacePointer, SDL_SRCCOLORKEY|SDL_RLEACCEL, 
                                 value);
            }
        }
    }

    /*******************************************************
     * Get Height of Surface
     *
     * Returns:
     *     Height of this surface.
     ******************************************************/
    public int height () {return mSurfacePointer.h;}

    /*******************************************************
     * Get Width of Surface
     *
     * Returns:
     *     Width of this surface.
     ******************************************************/
    public int width ()  {return mSurfacePointer.w;}

    /*******************************************************
     * Set Alpha Value of Surface
     *
     * Params:
     *     alpha = Alpha value of this surface.
     ******************************************************/
    public void alpha (ubyte alpha) 
    {
        int success = SDL_SetAlpha (mSurfacePointer, 
                                    SDL_SRCALPHA, alpha);

        if (success == -1) {
            Stdout ("Cannot set alpha").newline;
        }

        convertAlpha ();
    }

    /*******************************************************
     * Get Alpha Value of Surface
     *
     * Returns:
     *     Alpha value of this surface.
     ******************************************************/
    public ubyte alpha ()
    {
        return mSurfacePointer.format.alpha;
    }

    /*******************************************************
     * Get SDL_Surface Pointer of Surface
     *
     * Returns:
     *    SDL_Surface * of current surface.
     ******************************************************/
    public SDL_Surface * surfacePointer () {return mSurfacePointer;}

    /******************************************************
     * Cleanup and Free SDL_Surface Memory
     ******************************************************/
    public ~this ()
    {
        // In case that DerelictSDL.unload is called before
        // GC runs.
        DerelictSDL.load ();

        // Free SDL_Surface memory
        SDL_FreeSurface (mSurfacePointer);
        mSurfacePointer = null;
    }

    /******************************************************
     * Convert SDL_Surface to Display Format
     ******************************************************/
    private void convertAlpha ()
    {
        SDL_Surface * tmp = SDL_DisplayFormat (mSurfacePointer);

        if (tmp is null) {
            Stdout ("Cannot set alpha").newline;
        }

        SDL_FreeSurface (mSurfacePointer);
        mSurfacePointer = tmp;
    }

    /*********************************************************
     * Get a pixel in image.
     *
     * Params:
     *     surface = SDL_Surface pointer of image.
     *     offset  = How many pixel offset from first pixel
     *
     * Returns:
     *     The pixel value in 64 bit unsigned integer.
     ********************************************************/
    private ulong getPixel (SDL_Surface * surface, ulong offset)
    {
        uint pixel;
        int bytesPerPixel = surface.format.BytesPerPixel;

        SDL_LockSurface(surface);

        // Just in case that someone pass in a non 32-bit RGBA PNG file,
        // this would prevent program from crash.
        switch (bytesPerPixel) {
            case 1:
                pixel = *(cast(ubyte *) surface.pixels + offset);
                break;

            case 2:
                pixel = *(cast(ushort *) surface.pixels + offset);
                break;

            //! 24-bit PNG would match this.
            //! This will not go through all pixels.
            case 3:
                pixel = *(cast(ushort *) surface.pixels + offset);
                break;

            case 4:
                pixel = *(cast(uint *) surface.pixels + offset);
                break;

            case 8:
                pixel = *(cast(long *) surface.pixels + offset);
                break;

            default:
                pixel = *(cast(ubyte *) surface.pixels + offset);
        }
        SDL_UnlockSurface(surface);

        return pixel;
    }

    /******************************************************
     * Find Color of Transparent Background
     *
     * This method will scan through image to find a pixel
     * that mask as transparent, and return the color of
     * it.
     *
     * Params
     *     surface = The surface of image.
     *     color   = The color key if find.
     *
     * Returns:
     *     Did we find a color key?
     ******************************************************/
    private bool findColorKey (SDL_Surface * surface, ref uint colorkey)
    {
        // Calcuate how many pixels we have
        int length = surface.w * surface.h;
        bool found = false;
        Stdout.format ("bytes:{}", surface.format.BytesPerPixel).newline;

        // Go through all pixels.
        for (int i = 0; i < length; i++) {

            // Get current pixel data from image.
            ulong pixel = getPixel (surface, i);

            // Get RGBA of current pixel
            ubyte red, blue, green, alpha;
            SDL_GetRGBA (pixel, surface.format, &red, &green, 
                         &blue, &alpha);
    
            // If this is transparent pixel, return the 
            // current color as color key.
            if (alpha == 0) {
                found = true;
                colorkey = SDL_MapRGBA (surface.format, red, green, 
                                        blue, alpha);
            }
        }

        return found;
    }
}

/**************************************************
 * SDL Display
 *
 * This class represent a main window of SDL App.
 *************************************************/
class SDLDisplay
{
    private SDL_Surface * mSurfacePointer; // SDL Surface of window.

    private SDLLayer [] mLayers;       // Layers of current window.
    private int [String] mNameToLayer; // Layer name to layer index hash table.

    /*****************************************************
     * SDL Layer Implement
     *
     * This class implement SDLLayer interface
     ****************************************************/
    class SDLLayerI : SDLLayer
    {
        private SDLSurface mSurface;  // SDL Surface of this layer

        private String mName;   // Name of this layer.

        private int mX;         // Top-left X Coordinate of this layer.
        private int mY;         // Top-left Y Coordinate of this layer.

        private bool mValid;    // Is this layer valid?
        private bool mHide;     // Is this layer hideen?

        /**************************************************
         * Create SDL Layer
         * 
         * Params:
         *     surface = This SDL Surface of this layer.
         *     name = Name of this layer.
         *     x = Top-left X coordinate of this layer.
         *     y = Top-left Y coordinate of this layer.
         ************************************************/
        public this (SDLSurface surface, String name, int x, int y)
        in
        {
            assert (surface !is null);
            assert (name !is null);
        }
        body
        {
            mX = x;
            mY = y;

            mName = name;
            mSurface = surface;
            mValid = true;
        }

        /***************************************************
         * Move Layer to (X,Y).
         *
         * Params:
         *     x = New Top-Left X Coordinate of this layer.
         *     y = New Top-Left Y Coordinate of this layer.
         **************************************************/
        public void move (int x, int y)
        {
            mX = x;
            mY = y;
        }

        /***************************************************
         * Move Animation (X-Axis)
         *
         * Params:
         *     offset = Offset of move in pixel
         *     step   = How many pixel a frame
         **************************************************/
        public void moveX (int offset, uint step)
        {
            int boundry = offset >= 0 ? offset : -offset;
            int origX = this.mX;

            for (int i = 0; i < boundry; i+=step)
            {
                this.mX = (offset >= 0) ? this.mX + step : this.mX - step;
                render ();
            }

            this.mX = origX + offset;
            render ();
        }
    
        /***************************************************
         * Move Animation (Y-Axis)
         *
         * Params:
         *     offset = Offset of move in pixel
         *     step   = How many pixel a frame
         **************************************************/
        public void moveY (int offset, uint step)
        {
            int boundry = offset >= 0 ? offset : -offset;
            int origY = this.mY;

            for (int i = 0; i < boundry; i+=step)
            {
                this.mY = (offset >= 0) ? this.mY + step : this.mY - step;
                render ();
            }

            this.mY = origY + offset;
            render ();
        }
    
        /***************************************************
         * Move Animation (Both X-Axis and Y-Axis)
         *
         * Params:
         *     offset = Offset of move in pixel
         *     step   = How many pixel a frame
         **************************************************/
        public void moveXY (int offsetX, int offsetY, uint stepX, uint stepY)
        {
            int boundryX = (offsetX >= 0) ? offsetX : -offsetX;
            int boundryY = (offsetY >= 0) ? offsetY : -offsetY;

            int counterX = 0;
            int counterY = 0;

            int origX = this.mX;
            int origY = this.mY;

            while (counterX < boundryX || counterY < boundryY) {
                if (counterX < boundryX) {
                    this.mX = (offsetX >= 0) ? this.mX + stepX: 
                                               this.mX - stepX;
                    counterX++;
                }

                if (counterY < boundryY) {
                    this.mY = (offsetY >= 0) ? this.mY + stepY: 
                                               this.mY - stepY;
                    counterY++;
                }
                render ();
            }

            this.mX = origX + offsetX;
            this.mY = origX + offsetY;
            render ();
        }

        /**************************************************
         * Fade Out
         *
         * Params:
         *     rate = The fade out rate
         *************************************************/
        public void fadeOut (float rate = 1)
        in
        {
            assert (rate > 0, "Rate should larger than 0");
        }
        body
        {
            int step = cast(int) (10 * rate);
            step = (step == 0) ? 1 : step;

            for (int i = this.alpha; i > Alpha.TRANSPARENT; i-= step) {
                this.alpha = i;
                render ();
            }

            this.alpha = Alpha.TRANSPARENT;
            render ();
            
            this.hide = true;
        }

        /**************************************************
         * Fade In
         *
         * Params:
         *     rate = The fade out rate
         *************************************************/
        public void fadeIn (float rate = 1)
        in
        {
            assert (rate > 0, "Rate should larger than 0");
        }
        body
        {
            int step = cast(int) (10 * rate);
            step = (step == 0) ? 1 : step;

            this.hide = false;

            for (int i = this.alpha; i < Alpha.OPAQUE; i += step) {
                this.alpha = i;
                render ();
            }

            this.alpha = Alpha.OPAQUE;
            render ();
        }

        /***************************************************
         * Remove Layer
         **************************************************/
        public void remove ()
        {
            int index = mNameToLayer[name];
            mLayers[index] = null;
            mValid = false;
        }

        /*******************************************************
         * Set Alpha of Layer
         *
         * Params:
         *     aplha = Alpha value for layer, 0 is transparent
         *             and 255 is opaque.
         ******************************************************/
        public void alpha (ubyte alpha) 
        {
            mSurface.alpha = alpha;
        }

        /***************************************************
         * Get Alpha of Layer
         *
         * Returns:
         *     Alpha value of this layer.
         **************************************************/
        public ubyte alpha () {return mSurface.alpha;}

        /***************************************************
         * Hide/Show Layer.
         *
         * Params:
         *     isHide = Should this layer be hidden?
         **************************************************/
        public void hide (bool isHide)
        {
            mHide = isHide;
        }

        /***************************************************
         * Is this layer hidden?
         *
         * Returns:
         *     Return true if this layer is hidden, 
         *     otherwise return false.
         **************************************************/
        public bool hide () {return mHide;}

        /***************************************************
         * Get Layer Top-Left X Coordinate
         *
         * Returns:
         *     Top-Left X Coordinate of this layer.
         **************************************************/
        public int x () {return mX;}

        /***************************************************
         * Get Layer Top-Left Y Coordinate
         *
         * Returns:
         *     Top-Left Y Coordinate of this layer.
         **************************************************/
        public int y () {return mY;}
        
        /***************************************************
         * Get Height of Layer
         *
         * Returns:
         *     Height of this layer.
         **************************************************/
        public uint height () {return mSurface.surfacePointer.h;}

        /***************************************************
         * Get Width of Layer
         *
         * Returns:
         *     Width of this layer.
         **************************************************/
        public uint width () {return mSurface.surfacePointer.w;}

        /***************************************************
         * Is this layer valid?
         *
         * Returns:
         *     If this layer has been removed, return true.
         *     Otherwise, return false.
         **************************************************/
        public bool valid () {return mValid;}

        /**************************************************
         * Get Name of Layer
         *
         * Returns:
         *     Name of this layer.
         *************************************************/
        public String name () {return mName;}

        /**************************************************
         * SDLSurface of layer
         *
         * Returns:
         *     The SDL Surface of this layer.
         *************************************************/
        public SDLSurface surface () {return mSurface;}

        public ~this ()
        {
            mSurface = null;
        }

    }

    /**********************************************************
     * SDLDisplay Invariant
     * 
     * After SDLDisplay has been created, mSurfacePointer
     * should not be null at all time.
     *********************************************************/
    invariant 
    {
        assert (mSurfacePointer !is null);
    }

    /**********************************************************
     * Create a New SDLDisplay
     *
     * Params:
     *     width  = Width of new window.
     *     height = Height of new window.
     *     bitsPerPixel = Bits per pixel.
     *     flags  = Flags to SDL.
     *              Default is SDL_HWSURFACE | SDL_DOUBLEBUF
     *********************************************************/
    public this (int width, int height, 
                 int bitsPerPixel = 32, 
                 uint flags = SDL_HWSURFACE|SDL_DOUBLEBUF|SDL_SRCALPHA)
    {
        mSurfacePointer = SDL_SetVideoMode (width, height, 
                                            bitsPerPixel, flags);

        if (mSurfacePointer is null) {
            auto message = fromStringz (SDL_GetError());
            throw new Exception ("Cannot create an display:" ~ message);
        }
    }

    /**********************************************************
     * Set Window Title
     *
     * Params:
     *     normalTitle = Title shows at window title bar.
     *     iconTitle   = Title shows at taskbar.
     *********************************************************/
    public void setTitle (String normalTitle, String iconTitle = null)
    {
        if (iconTitle is null) {
            iconTitle = normalTitle;
        }

        SDL_WM_SetCaption (toStringz(normalTitle), toStringz(iconTitle));
    }

    /**********************************************************
     * Add a Layer into Window
     *
     * Params:
     *     surface = The surface to create layer.
     *     name    = Name of new layer.
     *     x       = The top-left X coordinate of new layer.
     *     y       = The top-left Y coordinate of new layer.
     *
     * Returns:
     *     The reference to new layer.
     *********************************************************/
    public SDLLayer addLayer (SDLSurface surface, String name, 
                              int x = 0, int y = 0)
    {
        if (surface is null) {
            throw new Exception ("Layer couldn't have a null layer");
        }

        if (name is null) {
            throw new Exception ("Layer should have a name");
        }

        if (name in mNameToLayer) {
            throw new Exception ("There already a layer named:" ~ name);
        }

        SDLLayer newLayer = new SDLLayerI (surface, name, x, y);
        
        // Add a layer to queue.
        int lastIndex      = mLayers.length;
        mLayers.length     = lastIndex + 1;
        mLayers[lastIndex] = newLayer;

        // Map its index in hash table.
        mNameToLayer[name] = lastIndex;

        return newLayer;
    }

    /**********************************************************
     * Get Layer From Name
     *
     * Params:
     *     name = Name of layer.
     *
     * Returns:
     *     The reference to new layer or null if there is no
     *     correspoding layer to name.
     *********************************************************/
    public SDLLayer getLayer (String name)
    {
        if ((name in mNameToLayer) is null) {
            return null;
        }

        int index = mNameToLayer[name];
        return mLayers[index];
    }

    /**********************************************************
     * Update Main Window.
     *
     * This method will update the content of window.
     *********************************************************/
    public void render ()
    {
        // Clear screen.
        SDL_FillRect (mSurfacePointer, null, 0);

        foreach (layer; mLayers)
        {
            if (layer is null || layer.hide) {
                continue;
            }

            renderLayer (layer);
        }

        SDL_Flip(mSurfacePointer);
    }

    /**********************************************************
     * Render a Layer
     *
     * This method will render a layer to main window, it just
     * paste the layer to the main window.
     *
     * It will not showed until you called SDLDisplay.render()
     * to update the window.
     *
     * Params:
     *     layer = Layer to be rendered.
     *********************************************************/
    private void renderLayer (SDLLayer layer)
    {
        SDL_Rect destRect;
        destRect.x = layer.x;
        destRect.y = layer.y;

        SDL_BlitSurface (layer.surface.surfacePointer, null, 
                         mSurfacePointer, &destRect);
    }
}
